package org.springframework.data.jpa.repository.query;

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.Arrays;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;

import javax.persistence.EntityManager;
import javax.persistence.Query;

import org.springframework.core.GenericTypeResolver;
import org.springframework.data.jpa.repository.query.JpaParameters.JpaParameter;
import org.springframework.data.repository.core.RepositoryMetadata;
import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
import org.springframework.data.repository.query.ResultProcessor;
import org.springframework.data.repository.query.ReturnedType;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.wheel.base.stmt.OrmSqlUtils;
import org.wheel.base.stmt.SqlStatement;
import org.wheel.base.utils.ReflectionUtils;
import org.wheel.base.utils.StringUtils;

/**
 * repository sql 解析
 * 
 * @author klaus.wang
 *
 */
public class WheelQuery extends AbstractJpaQuery {

	/**
	 * 均为内部类, 用反射方式执行初始化
	 */
	public static Object INSTANCE;

	public static Method lookupFrom;
	public static Method fromQueryAnnotation;
	public static Method fromProcedureAnnotation;
	public static Method fromMethodWithQueryString;

	public static Constructor<PartTreeJpaQuery> partTreeJpaQueryConstructor;

	static ThreadLocal<QueryAndParams> queryAndParamsThreadLocal = new ThreadLocal<>();

	static {

		try {
			Class<?> JpaQueryFactory = Class.forName("org.springframework.data.jpa.repository.query.JpaQueryFactory");
			Field field = ReflectionUtils.findField(JpaQueryFactory, "INSTANCE");
			ReflectionUtils.makeAccessible(field);
			INSTANCE = field.get(null);

			fromQueryAnnotation = ReflectionUtils.findMethod(JpaQueryFactory, "fromQueryAnnotation",
					JpaQueryMethod.class, EntityManager.class, QueryMethodEvaluationContextProvider.class);

			ReflectionUtils.makeAccessible(fromQueryAnnotation);

			fromProcedureAnnotation = ReflectionUtils.findMethod(JpaQueryFactory, "fromProcedureAnnotation",
					JpaQueryMethod.class, EntityManager.class);
			ReflectionUtils.makeAccessible(fromProcedureAnnotation);

			fromMethodWithQueryString = ReflectionUtils.findMethod(JpaQueryFactory, "fromMethodWithQueryString",
					JpaQueryMethod.class, EntityManager.class, String.class,
					QueryMethodEvaluationContextProvider.class);
			ReflectionUtils.makeAccessible(fromMethodWithQueryString);

			Class<?> NamedQuery = Class.forName("org.springframework.data.jpa.repository.query.NamedQuery");

			lookupFrom = ReflectionUtils.findMethod(NamedQuery, "lookupFrom", JpaQueryMethod.class,
					EntityManager.class);
			ReflectionUtils.makeAccessible(lookupFrom);

			partTreeJpaQueryConstructor = ReflectionUtils.accessibleConstructor(PartTreeJpaQuery.class,
					JpaQueryMethod.class, EntityManager.class, EscapeCharacter.class);

		} catch (Exception e) {
			e.printStackTrace();
		}

	}

	private static Set<Class<?>> baseTypes;

	static {
		baseTypes = new HashSet<>();

//		baseTypes.add(Object.class);
		baseTypes.add(int.class);
		baseTypes.add(Integer.class);

		baseTypes.add(char.class);
		baseTypes.add(String.class);

		baseTypes.add(Number.class);
		baseTypes.add(Date.class);

		baseTypes.add(float.class);
		baseTypes.add(Float.class);

		baseTypes.add(double.class);
		baseTypes.add(Double.class);

		baseTypes.add(boolean.class);
		baseTypes.add(Boolean.class);

		baseTypes.add(long.class);
		baseTypes.add(Long.class);

		baseTypes.add(BigInteger.class);
		baseTypes.add(BigDecimal.class);

		// 集合
		baseTypes.add(List.class);
		baseTypes.add(Set.class);
		baseTypes.add(Collection.class);

		baseTypes.add(Object[].class);
		// 数组
		baseTypes.add(int[].class);
		baseTypes.add(Integer[].class);

		baseTypes.add(char[].class);
		baseTypes.add(String[].class);

		baseTypes.add(Number[].class);
		baseTypes.add(Date[].class);

		baseTypes.add(float[].class);
		baseTypes.add(Float[].class);

		baseTypes.add(double[].class);
		baseTypes.add(Double[].class);

		baseTypes.add(boolean[].class);
		baseTypes.add(Boolean[].class);

		baseTypes.add(long[].class);
		baseTypes.add(Long[].class);

		baseTypes.add(BigInteger[].class);
		baseTypes.add(BigDecimal[].class);

	}

	private String methodName;

	private Integer domainIndex;

	private Map<Integer, String> placeholderMap;

	private SqlStatement statement;

	private SpelExpressionParser parser;

	private QueryMethodEvaluationContextProvider evaluationContextProvider;

	private final QueryParameterSetter.QueryMetadataCache metadataCache = new QueryParameterSetter.QueryMetadataCache();

	public WheelQuery(JpaQueryMethod method, EntityManager em, String methodName) {
		super(method, em);
		this.setMethodName(methodName);
		this.init();
	}

	private void init() {
		placeholderMap = new HashMap<>();

		JpaQueryMethod method = this.getQueryMethod();

		RepositoryMetadata metadata = (RepositoryMetadata) ReflectionUtils.accessibleFieldValue(method, "metadata");

		Map<TypeVariable, Type> source = GenericTypeResolver.getTypeVariableMap(metadata.getRepositoryInterface());

		Set<Class<?>> sets = new HashSet<>(
				Arrays.asList(this.getQueryMethod().getEntityInformation().getJavaType(), Map.class));
		int size = this.getQueryMethod().getParameters().getNumberOfParameters();

		Method jpaMethod = (Method) ReflectionUtils.accessibleFieldValue(this.getQueryMethod(), "method");

		for (int i = 0; i < size; i++) {
			JpaParameter jpaParameter = this.getQueryMethod().getParameters().getParameter(i);
			java.lang.reflect.Parameter parameter = jpaMethod.getParameters()[i];
			Type type = source.get(parameter.getParameterizedType());
			if (type == null) {
				type = jpaParameter.getType();
			}

			/**
			 * 获取 dao 接口上的参数名
			 */
			if (type != null && baseTypes.contains(type)) {
				placeholderMap.put(i, jpaParameter.getName().get());
			}
			/**
			 * MAP 或者对象类型
			 */
			if (domainIndex == null
					&& (sets.contains(jpaParameter.getType()) || (type != null && sets.contains(type)))) {
				domainIndex = i;
			}
		}

		this.statement = OrmSqlUtils.getById(methodName);
	}

	@Override
	protected Query doCreateQuery(JpaParametersParameterAccessor accessor) {
		QueryAndParams queryAndParams = getQueryAndParams(accessor.getValues());

		DeclaredQuery declaredQuery = queryAndParams.getQuery();
		String sortedQueryString = QueryUtils.applySorting(declaredQuery.getQueryString(), accessor.getSort(),
				declaredQuery.getAlias());
		ResultProcessor processor = getQueryMethod().getResultProcessor().withDynamicProjection(accessor);
		javax.persistence.Query q = createJpaQuery(declaredQuery, sortedQueryString, processor.getReturnedType());
		this.setValues(q, queryAndParams.getParams());

		QueryParameterSetter.QueryMetadata metadata = metadataCache.getMetadata(sortedQueryString, q);

		javax.persistence.Query query = ParameterBinderFactory.createQueryAwareBinder(getQueryMethod().getParameters(),
				declaredQuery, parser, evaluationContextProvider).bindAndPrepare(q, metadata, accessor);

		return query;
	}

	@Override
	protected Query doCreateCountQuery(JpaParametersParameterAccessor accessor) {
		QueryAndParams queryAndParams = getQueryAndParams(accessor.getValues());

		DeclaredQuery declaredQuery = queryAndParams.getQuery();

		String queryString = CountSqlParser.getSmartCountSql(declaredQuery.getQueryString());

		EntityManager em = getEntityManager();

		javax.persistence.Query query = this.getStatement().isNativeQuery() ? em.createNativeQuery(queryString)
				: em.createQuery(queryString, Long.class);

		this.setValues(query, queryAndParams.getParams());

		QueryParameterSetter.QueryMetadata metadata = metadataCache.getMetadata(queryString, query);

		ParameterBinderFactory
				.createQueryAwareBinder(getQueryMethod().getParameters(), declaredQuery, parser,
						evaluationContextProvider)
				.bind(metadata.withQuery(query), accessor, QueryParameterSetter.ErrorHandling.LENIENT);

		return query;
	}

	private QueryAndParams getQueryAndParams(Object[] values) {

		return Optional.ofNullable(queryAndParamsThreadLocal.get()).orElseGet(() -> {

			Map<String, Object> params = this.getParameter(values);

			QueryAndParams queryAndParams = new QueryAndParams(getDeclaredQuery(params, values), params);

			queryAndParamsThreadLocal.set(queryAndParams);
			return queryAndParams;
		});

	}

	/**
	 * 获取到对象参数
	 * 
	 * @param values
	 * @return
	 */
	private Map<String, Object> getParameter(Object[] values) {
		if (domainIndex != null && values.length >= domainIndex && values[domainIndex] != null) {

			Object object = values[domainIndex];

			if (object instanceof Map) {
				return (Map<String, Object>) object;
			}

			return org.springframework.cglib.beans.BeanMap.create(object);
		}
		return new HashMap<>();
	}

	protected DeclaredQuery getDeclaredQuery(Map<String, Object> params, Object[] values) {
		Map<String, Object> properties = null;
		if (!placeholderMap.isEmpty()) {
			properties = new HashMap<>();
			for (Map.Entry<Integer, String> entry : placeholderMap.entrySet()) {
				if (values[entry.getKey()] == null) {
					continue;
				}
				properties.put(entry.getValue(), values[entry.getKey()]);
			}
			// 单个参数覆盖对象里面的参数
			params.putAll(properties);
		}

		/**
		 * 解析动态sql 占位符处理. eg: ${name}
		 */
		String sql = StringUtils.replacePlaceholders(getStatement().execute(params), params);

		DeclaredQuery query = new ExpressionBasedStringQuery(sql, this.getQueryMethod().getEntityInformation(),
				this.parser);

		return query;
	}

	/**
	 * 参数赋值
	 * 
	 * @param query
	 * @param params
	 */
	private void setValues(Query query, Map<String, Object> params) {
		if (params.isEmpty()) {
			return;
		}
		Set<javax.persistence.Parameter<?>> sets = query.getParameters();
		for (javax.persistence.Parameter<?> parameter : sets) {
			Object value = params.get(parameter.getName());
//			Object value = params.getOrDefault(parameter.getName(),
//					parameter.getParameterType() == null ? Object.class : parameter.getParameterType());
			query.setParameter(parameter.getName(), value);
		}
	}

	protected javax.persistence.Query createJpaQuery(DeclaredQuery query, String queryString,
			ReturnedType returnedType) {
		EntityManager em = getEntityManager();
		if (query.hasConstructorExpression() || query.isDefaultProjection()) {
			return getStatement().isNativeQuery() ? em.createNativeQuery(queryString) : em.createQuery(queryString);
		}
		Class<?> typeToRead = null;
		if (!getStatement().isModify()) {
			typeToRead = getStatement().getResultType();
			if (typeToRead == null && (typeToRead = getTypeToRead(returnedType)) == null) {
				typeToRead = returnedType.getDomainType();
			}
		}
		boolean isNative = getStatement().isNativeQuery();
		javax.persistence.Query x = typeToRead == null
				? (isNative ? em.createNativeQuery(queryString) : em.createQuery(queryString))
				: (isNative ? em.createNativeQuery(queryString, typeToRead) : em.createQuery(queryString, typeToRead));
		return x;
	}

	@Override
	protected JpaQueryExecution getExecution() {
		if (getStatement().isModify()) {
			return new JpaQueryExecution.ModifyingExecution(this.getQueryMethod(), this.getEntityManager());
		}
		return super.getExecution();
	}

	@Override
	public Object execute(Object[] parameters) {

		try {
			queryAndParamsThreadLocal.remove();

			Object ret = super.execute(parameters);

			return ret;
		} finally {
			queryAndParamsThreadLocal.remove();
		}

	}

	public void setParser(SpelExpressionParser parser) {
		this.parser = parser;
	}

	public void setMethodName(String methodName) {
		this.methodName = methodName;
	}

	public void setEvaluationContextProvider(QueryMethodEvaluationContextProvider evaluationContextProvider) {
		this.evaluationContextProvider = evaluationContextProvider;
	}

	public SqlStatement getStatement() {
		return statement;
	}

}
